/*
 * KindlegenRunner.java
 *
 *  created: 30.8.2011
 *  charset: UTF-8
 *  license: MIT (X11) (See LICENSE file for full license)
 */
package cz.mp.k3bg.core;

import static cz.mp.k3bg.Application.EOL;
import cz.mp.k3bg.log.LoggerManager;
import cz.mp.util.Stopwatch;
import cz.mp.util.StringUtils;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

/**
 * Třída {@code KindlegenRunner} slouží pro vykonání programu {@code kindlegen}.
 * <p>
 * Testováno s {@code kindlegen} ve verzích: 1.1, 1.2, 2.3, 2.5.
 * Soubory knihy by měly být kódovány v {@code UTF-8}.
 * <p>
 * Příklad použití:
 * <tt><pre>
 * Kindlegen kindlegen = Kindlegen.getKindlegen("kindlegen");
 * if (kindlegen != null) {
 *     KindlegenRunner kindlegenRunner = new KindlegenRunner();
 *     kindlegenRunner.setKindlegen(kindlegen);
 *     kindlegenRunner.setOpfFileNamePath("mybook" + file.separator + "book.opf");
 *     kindlegenRunner.setOutputFileName("mybook.mobi");
 *     kindlegenRunner.setKindlegenCommandOutputStream(System.out);
 *     kindlegenRunner.run();
 *     System.err.println("kindlegen result: " +
 *             (kindlegenRunner.isInErrorState() ? "ERROR" : "OK"));
 * }
 * </pre></tt>
 * 
 * @author Martin Pokorný
 * @version 0.6.1
 */
public class KindlegenRunner {
    
    private static final boolean DEBUG = false;
    private static final Logger logger =
            LoggerManager.getLogger(KindlegenRunner.class, DEBUG);
    
    private Kindlegen kindlegen;
    
    /** Příznak zda {@code kindlegen} skončil s chybou 
     * (řádek obsahuje text "{@code Error}"). */
    private boolean inErrorState = false;

    /** Doba běhu samotného programu {@code kindlegen}. */
    private long runtime = -1;
        
    /** */
    private String opfFileNamePath;
    
    /** */
    private String outputFileName;
    
    /** Na tento proud se zapisuje výstup programu {@code kindlegen}. */
    private OutputStream kindlegenCommandOutputStream;
    
    /** Přítomnost přepínače {@literal -c2}. */
    private boolean c2compress = false;
    
    /** Proces OS, který vykonává {@code kindlegen} se zadanými parametry. */
    private Process process;

    // -----
    
    /**
     * 
     */
    public KindlegenRunner() {        
    }
    
    /**
     * 
     * @param opfFileNamePath
     * @param outputFileName 
     */
    public KindlegenRunner(String opfFileNamePath, String outputFileName) {
        setOpfFileNamePathImpl(opfFileNamePath);
        setOutputFileNameImpl(outputFileName);
    }
    
    /**
     * 
     * @param opfFileNamePath
     * @param outputFileName 
     */
    public KindlegenRunner(Kindlegen kindlegen, 
            String opfFileNamePath, String outputFileName) {
        setKindlegenImpl(kindlegen);
        setOpfFileNamePathImpl(opfFileNamePath);
        setOutputFileNameImpl(outputFileName);
    }
    
    /**
     * Vykoná {@code kindlegen} s parametry.
     * 
     * @return  řádky s výstupem programu {@code kindlegen}; viz 
     *      {@linkplain #setKindlegenCommandOutputStream(java.io.OutputStream)}.
     * @throws IOException 
     * @throws IllegalStateException
     */
    public String[] run() throws IOException {
        logger.info("");
        testStateForRun();
        
        // Sem se také zapisuje výstup programu {@code kindlegen}.
        List<String> outputLines = new ArrayList<String>();
        inErrorState = false;
        Stopwatch runtimeStopwatch = new Stopwatch();
        runtimeStopwatch.start();

        try {
            String[] realCommandArray = assembleKindlegenCommandArray();
            logger.info("command:  " + Arrays.toString(realCommandArray));
            
            String readableCommand = createReadableCommand(realCommandArray);
            // (zde možno zapsat readableCommand do skriptu ...)
            writeToCommandOut("> " + readableCommand);

            process = Runtime.getRuntime().exec(realCommandArray);

            InputStream istream = process.getInputStream();
            BufferedReader br = new BufferedReader(
                    new InputStreamReader(istream));

            String line;            
            while ((line = br.readLine()) != null) {
                checkErrorState(line);
                outputLines.add(line);
                logger.fine(line);
                writeToCommandOut(line);
            }

            try {
                process.waitFor();
            } catch (InterruptedException e) {
                setInErrorState(true);
                writeToCommandOut(EOL+"Error: InterruptedException!");
            }
//            if (proc.exitValue() != 0) {}      // nejde, protože kindlegen (v1.2) vrátí vždy 0
           
            br.close();
            
            logger.fine("finished!");
        } catch (IOException ex) {
            logger.warning(ex.toString());
            setInErrorState(true);
            runtime = runtimeStopwatch.stop();
            
            throw ex;
        }       
        runtime = runtimeStopwatch.stop();        
        logger.info("kindlegen runtime = " + 
                runtimeStopwatch.getTimeSec() + " s");
        
        return (String[]) outputLines.toArray(new String[0]);  
    }

    /**
     * 
     * @throws IllegalStateException
     * @see #run()
     */
    private void testStateForRun() {
        if (StringUtils.isBlank(opfFileNamePath)) {
            logger.warning("opfFileNamePath is empty!");
            throw new IllegalStateException("opfFileNamePath is empty!");
        }
        if (StringUtils.isBlank(outputFileName)) {
            logger.warning("outputFileName is empty!");
            throw new IllegalStateException("outputFileName is empty!");
        }  
        if (kindlegen == null) {
            logger.warning("kindlegen = null");
            throw new IllegalStateException("kindlegen = null");
        }        
    }
    
    /**
     * Zastaví vykonávání {@code kindlegen}, pokud běží.
     *
     * @see #run() 
     */
    public void stop() {
        if (process == null) {
            logger.fine("process=null; skip");
        }
        else {
            logger.info("");
            process.destroy();
            setInErrorState(true);
        }
    }

    /**
     * 
     * @return  Doba běhu programu {@code kindlegen}, nebo -1, 
     *      pokud nebyl spuštěn.
     */
    public long getRuntime() {
        return runtime;
    }

    /**
     * 
     * @param line 
     */
    private void checkErrorState(String line) {
        if (line == null) {
            throw new IllegalStateException("line is null!");
        } 
        if (line.toLowerCase().startsWith("error")) {
            setInErrorState(true);
        }
    }
    
    /**
     * 
     * @return 
     */
    public boolean isInErrorState() {
        return inErrorState;
    }

    /**
     * 
     */
    private void setInErrorState(boolean inErrorState) {
        logger.fine("inErrorState = " + inErrorState);
        this.inErrorState = inErrorState;
    }
    

    /**
     * 
     * @param line
     * @throws IOException 
     */
    private void writeToCommandOut(String line) throws IOException {
        if (kindlegenCommandOutputStream != null) {
            kindlegenCommandOutputStream.write(line.trim().getBytes("UTF-8"));
            kindlegenCommandOutputStream.write(EOL.getBytes("UTF-8"));
        }
    }
    
    /**
     * 
     * @param kindlegen 
     * @throws IllegalArgumentException
     */
    public void setKindlegen(Kindlegen kindlegen) {
        setKindlegenImpl(kindlegen);
    }
    
    /**
     * 
     * @param kindlegen 
     * @throws IllegalArgumentException
     */    
    private void setKindlegenImpl(Kindlegen kindlegen) {
        if (kindlegen == null) {
            throw new IllegalArgumentException("kindlegen = null");
        }
        this.kindlegen = kindlegen;
    }    

    /**
     * 
     * @return 
     */
    public Kindlegen getKindlegen() {
        return kindlegen;
    }

    /**
     * Sestaví příkaz pro spuštění programu {@code kindlegen} s
     * parametry podle této třídy.
     *
     * @return
     */
    private String[] assembleKindlegenCommandArray() {
        if (kindlegen == null) {
            logger.warning("kindlegen = null");
            throw new IllegalArgumentException("kindlegen = null");
        }
        
        logger.fine("");

        ArrayList<String> cmds = new ArrayList<String>();
        cmds.add(kindlegen.getCommand());

        if (opfFileNamePath != null && ! opfFileNamePath.isEmpty()) {
            if (c2compress) {
                cmds.add(Kindlegen.C2_OPT);
            }
            if (kindlegen.isNoUnicodeVersion()) {
                cmds.add(Kindlegen.FORCE_UNICODE_OPT);
            }
            cmds.add(opfFileNamePath);
            if (outputFileName != null && ! outputFileName.isEmpty()) {
                cmds.add(Kindlegen.O_OPT);
                cmds.add(outputFileName);
            }
        }
        else {
            return new String[0];
        }

        return cmds.toArray(new String[0]);
    }
    
    /**
     * Části příkazu sloučí do jednoho textu.
     * Tento text lze zkopírovat do příkazové řádky, zalogovat, a pod.
     * 
     * @param array
     * @return  příkaz v podobě, kterou lze předat příkazovému řádku, nebo
     *      prázdný řetězec
     * @see #assembleKindlegenCommandArray() 
     */
    private static String createReadableCommand(String[] array) {
        StringBuilder sb = new StringBuilder();
        for (String cmdPart : array) {
            if (cmdPart.contains(" ")) {
                sb.append("\"").append(cmdPart).append("\"");
            }
            else {
                sb.append(cmdPart);
            }
            sb.append(" ");
        }
        return sb.toString();
    }
    
    /**
     * 
     * @return 
     */
    public String getOutputFileName() {
        return outputFileName;
    }
    
    /**
     * 
     */
    public void setOutputFileName(String outputFileName) {
        setOutputFileNameImpl(outputFileName);
    }

    /**
     * 
     */
    private void setOutputFileNameImpl(String outputFileName) {
        if (outputFileName == null) {
            throw new IllegalArgumentException("outputFileName=null");
        }
        logger.config("outputFileName = " + outputFileName);
        this.outputFileName = outputFileName;
    }

    /**
     * 
     * @return 
     */
    public String getOpfFileNamePath() {
        return opfFileNamePath;
    }

    /**
     * 
     * @param opfFileNamePath 
     */
    public void setOpfFileNamePath(String opfFileNamePath) {
        setOpfFileNamePathImpl(opfFileNamePath);
    }

    /**
     * 
     * @param opfFileNamePath 
     */
    private void setOpfFileNamePathImpl(String opfFileNamePath) {
        if (opfFileNamePath == null) {
            throw new IllegalArgumentException("opfFileNamePath=null");
        }        
        logger.config("opfFileNamePath = " + opfFileNamePath);
        this.opfFileNamePath = opfFileNamePath;
    }    
    
    /**
     * 
     * @return 
     */
    public boolean isC2compress() {
        return c2compress;
    }

    /**
     * Nastavit vyšší kompresi. Odpovídá parametru {@literal -c2} 
     * programu {@code kindlegen}.
     * Způsobí pomalé sestavení.
     * 
     * @param c2compress 
     */
    public void setC2compress(boolean c2compress) {
        this.c2compress = c2compress;
    }

    /**
     * 
     * @return 
     */
    public OutputStream getKindlegenCommandOutputStream() {
        return kindlegenCommandOutputStream;
    }

    /**
     * Nastaví výstupní proud do něhož se bude průběžně zapisovat
     * std výstup programu {@code kindlegen} během vykonávání.
     * 
     * @param kindlegenCommandOutputStream  (může být např. System.out)
     */
    public void setKindlegenCommandOutputStream(
            OutputStream kindlegenCommandOutputStream) {
        if (kindlegenCommandOutputStream == null) {
            throw new IllegalArgumentException(
                    "kindlegenCommandOutputStream=null");
        }           
        this.kindlegenCommandOutputStream = kindlegenCommandOutputStream;
    }
    
}   // KindlegenRunner
